Repository navigation
fix(trust-gate): prevent propose_truth from hanging indefinitely on LLM timeout - #19
Merged
Merged
Conversation
…LM timeout Three-layer defense against the 12-hour hang (session b3147494): 1. asyncio.wait_for(timeout=120) wraps the Trust Gate LLM call in handle_propose_truth — fires after 2 minutes and returns a structured error, preventing infinite MCP server block regardless of provider behavior. 2. Explicit httpx.Timeout decomposition in LLMClient.chat — replaces the scalar timeout with httpx.Timeout(connect=10, read=N, write=N, pool=10) so all httpx timeout phases are explicitly bounded (scalar timeout leaves the pool timeout as None/infinite). 3. Rotating file handler for MCP server logs — persists to ~/.fava-trails/logs/mcp-server.log so server-side hangs are diagnosable without relying on Claude Code session.jsonl. Adds regression test: test_propose_truth_timeout_returns_error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `tool_timeout_secs` (default 300) and `trust_gate_timeout_secs` (default 120) to GlobalConfig — set either to 0 to disable - Introduce `@with_tool_timeout` decorator on `handle_call_tool`: wraps every tool dispatch in asyncio.wait_for, reads config at call time so changes don't require a server restart - Replace hardcoded 120.0 in handle_propose_truth with `global_config.trust_gate_timeout_secs` - Add tests: decorator pass-through, timeout error dict, disabled-when-zero Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HIGH: honour trust_gate_timeout_secs=0 as "disabled" in handle_propose_truth (previously always called asyncio.wait_for, raising immediately on 0.0) - MEDIUM: wrap rotating file handler setup in try/except OSError so a restricted filesystem never prevents server startup (falls back to stderr) - MEDIUM: lock _get_trail with asyncio.Lock + double-check to prevent double-init race when parallel calls create the same new scope simultaneously - LOW: add httpx>=0.27.0 as explicit dependency in pyproject.toml - LOW: add model_validator to GlobalConfig enforcing trust_gate_timeout_secs < tool_timeout_secs (when both > 0) with NonNegativeInt constraints on both - Tests: validator happy/sad paths, disable-when-zero path, lock race coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
timeleft--
force-pushed
the
fix/propose-truth-hang
branch
from
March 10, 2026 12:09
55d439b to
4ec16b4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
propose_truthcalling OpenRouter viaasyncio.wait_forwith 60s timeout, butclient_args={"timeout": scalar}left the httpx pool timeout unset (infinite). OpenRouter accepted the connection then never responded — 12+ hour hang in session b3147494.@with_tool_timeoutdecorator onhandle_call_tool— wraps every MCP tool call inasyncio.wait_for, readstool_timeout_secsfrom config at call time. Default: 300s.asyncio.wait_foraround the Trust Gate LLM call specifically, using configurabletrust_gate_timeout_secs. Default: 120s. Returns structured error on timeout instead of hanging indefinitely.httpx.Timeout(connect=10, read=N, write=N, pool=10)— sets all httpx timeout phases explicitly so no phase defaults to infinite.~/.fava-trails/logs/mcp-server.log. Without this, server-side hangs are undiagnosable since Claude Code doesn't capture MCP stderr to disk.Configuration
Both timeouts are configurable in
config.yaml, set to0to disable:Test plan
test_propose_truth_timeout_returns_error— Trust Gate LLM timeout returns structured errorTestWithToolTimeout::test_timeout_returns_error_dict— decorator fires after configurable timeoutTestWithToolTimeout::test_passes_through_on_success— no interference on normal callsTestWithToolTimeout::test_disabled_when_zero— timeout disabled whentool_timeout_secs=0🤖 Generated with Claude Code